// ****************************************************************
// Copyright 2012, Stephan Burguchev
// e-mail: me@sburg.net
// ****************************************************************
// *
using System;
using System.ComponentModel;
using System.Configuration;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Updater.Common;
using Updater.Sources;
using Updater.Updates;
using Updater.Utils;
using Updater.Wpf.Controls;
using Updater.Wpf.Properties;
using Updater.Wpf.Utils;
using Configuration = Updater.Common.Configuration;

namespace Updater.Wpf.ViewModels
{
    public class UpdaterViewModel : INotifyPropertyChanged
    {
        public const string FinalUpdateResultsLoggerKey = "FinalUpdateResults";
        private const string UpdaterExeResourceName = "Updater.Wpf.Assemblies.Updater.exe";
        private static readonly TimeSpan UpdateStatusLifeTime = TimeSpan.FromSeconds(30);
        private static readonly TimeSpan UpdatesCheckInitialDelay = TimeSpan.FromSeconds(5);
        private readonly ILogger logger;
        private readonly IMessageBoxService msgBoxService;
        private Action showStatusDetailsHandler;

        private UpdateStatus status;
        private string statusText;
        private DispatcherTimer timer;
        private bool userInteractionIsRequired;

        /// <summary>Constructor.</summary>
        /// <exception cref="FormatException">
        ///     Thrown when the updates feed URI property is not set in the app.config file.
        /// </exception>
        /// <param name="msgBoxService">
        ///     (optional) The message box service. The standard WPF message box will be used if the
        ///     parameter is omitted.
        /// </param>
        /// <param name="logger">
        ///     (optional) The logger. Log messages will be directed into the Console if the parameter is
        ///     omitted.
        /// </param>
        public UpdaterViewModel(IMessageBoxService msgBoxService = null, ILogger logger = null)
        {
            if (msgBoxService == null)
            {
                msgBoxService = new WpfMessageBoxService();
            }

            if (logger == null)
            {
                logger = new SimpleConsoleLogger();
            }

            this.msgBoxService = msgBoxService;
            this.logger = logger;

            try
            {
                this.ShowStatusDetailsCommand = new DelegateCommand(
                    () => (this.ShowStatusDetailsHandler ?? delegate { })(), () => this.ShowStatusDetailsHandler != null);
                this.DownloadAndInstallUpdateCommand = new DelegateCommand(
                    this.DownloadAndInstallUpdate, () => this.CanDownloadAndInstallUpdate);
                this.ShowUpdateDetailsCommand = new DelegateCommand(
                    this.ShowUpdateDetails, () => this.CanShowUpdateDetails);
                this.RestartApplicationCommand = new DelegateCommand(this.RestartApplication);
                this.HideCommand = new DelegateCommand(() => this.UserInteractionIsRequired = false);

                this.ResetStatus();
                var configuration = new Configuration
                    {
                        UpdatesFeedUriString = ConfigurationManager.AppSettings["updater.updates-feed-uri"]
                    };

                if (configuration.UpdatesFeedUri == null)
                {
                    throw new FormatException(
                        "The app.config file does not contain a valid 'updater.updates-feed-uri' application settings property.");
                }

                string username = ConfigurationManager.AppSettings["updater.web-source.username"];
                string password = ConfigurationManager.AppSettings["updater.web-source.password"];
                TimeSpan updatesCheckInternal =
                    TimeSpan.FromSeconds(int.Parse(ConfigurationManager.AppSettings["updater.updates-check-internal"]));

                this.UpdateManager = new UpdateManager(
                    configuration,
                    new WebSource(username, password)
                        {
                            AlwaysAcceptRemoteCertificate = true
                        },
                    new UpdatesRssFeedReader());

                this.UpdateManager.UpdatesMonitoringError += this.OnUpdatesMonitoringError;
                this.UpdateManager.UpdatesDetected += this.OnUpdatesDetected;
                this.UpdateManager.UpdatesCheckPerformed += delegate(object o, UpdatesCheckPerformedEventArgs args)
                {
                    if (!args.IsUpdateRequired && this.Status == UpdateStatus.MonitoringError)
                    {
                        this.ResetStatus();
                    }
                };

                this.UpdateManager.UpdaterAssemblyRequested += delegate(object o, UpdaterAssemblyRequestedEventArgs args)
                {
                    using (args.Sink)
                    {
                        Assembly.GetExecutingAssembly().GetManifestResourceStream(UpdaterExeResourceName).CopyTo(args.Sink);
                        args.Handled = true;
                    }
                };

                UpdateLog log = this.UpdateManager.GetJustInstalledUpdateLog();
                if (log != null)
                {
                    if (log.Succeed)
                    {
                        this.StatusText = Resources.UpdateHasSuccessfullyInstalled;
                        this.Status = UpdateStatus.UpdateSucceed;
                        this.ShowStatusDetailsHandler =
                            this.UpdateSuccessDetailsDisplay != null
                                ? () =>
                                  this.UpdateSuccessDetailsDisplay(
                                      this,
                                      new UpdateDetailsDisplayEventArgs(this.UpdateManager.Configuration.CurrentVersion, log))
                                : (Action)(() => this.ShowUpdateSuccessDetails(log.ChangeLog));

                        string reportMessage =
                            string.Format(
                                "The application has been successfully updated to the version {0}.",
                                this.UpdateManager.Configuration.CurrentVersion);
                        logger.GetLogger(FinalUpdateResultsLoggerKey).Info(reportMessage);
                    }
                    else
                    {
                        this.StatusText = Resources.UpdateHasFailed;
                        this.Status = UpdateStatus.UpdateFailed;
                        this.ShowStatusDetailsHandler =
                            this.UpdateFailureDetailsDisplay == null
                                ? () => msgBoxService.Error(log.ErrorDescription)
                                : (Action)(() => this.UpdateFailureDetailsDisplay(
                                    this,
                                    new UpdateDetailsDisplayEventArgs(this.UpdateManager.Configuration.CurrentVersion, log)));

                        string reportMessage = "The application update has been failed!\r\n" +
                                               "\r\n" +
                                               string.Format(
                                                   "Current version: {0}\r\n", this.UpdateManager.Configuration.CurrentVersion) +
                                               "\r\n" +
                                               string.Format("Error description: {0}\r\n", log.ErrorDescription) +
                                               "\r\n" +
                                               string.Format("Error detalization: {0}\r\n", log.ErrorDetalization);
                        logger.GetLogger(FinalUpdateResultsLoggerKey).Error(reportMessage, new Exception());
                    }

                    // Clear status after N seconds if its still the same status.
                    this.timer = new DispatcherTimer
                        {
                            Interval = UpdateStatusLifeTime
                        };
                    this.timer.Tick += (o, args) =>
                    {
                        if (this.Status == UpdateStatus.UpdateSucceed || this.Status == UpdateStatus.UpdateFailed)
                        {
                            this.ResetStatus();
                        }

                        this.timer.Stop();
                    };
                    this.timer.Start();
                }

                this.UpdateManager.CleanUpUpdaterCacheDirectory();
                this.UpdateManager.StartUpdatesMonitoring(updatesCheckInternal, UpdatesCheckInitialDelay);
            }
            catch (Exception ex)
            {
                this.Status = UpdateStatus.InitializationError;
                this.StatusText = Resources.UpdaterInitializationError;
                this.ShowStatusDetailsHandler =
                    this.ErrorDetailsDisplay == null
                        ? () => msgBoxService.Error(this.StatusText + " " + ex.GetFullMessage())
                        : (Action)(() => this.ErrorDetailsDisplay(
                            this,
                            new ErrorDetailsDisplayEventArgs(ex, this.StatusText + " " + ex.GetFullMessage(), this.Status)));
            }
        }

        /// <summary>
        ///     Subscribe to this event to implement "Restart now" button action. You can either call Close()
        ///     method of the Window object, TryClose() of the Caliburn.Micro screen or even crash the
        ///     application, whatever.
        /// </summary>
        public event EventHandler ApplicationRestartRequest = delegate { };

        /// <summary>
        ///     Subscribe to this event only if default implementation calling IMessageBoxService.Error method
        ///     to display updater-related errors does not fit your needs.
        /// </summary>
        public event EventHandler<ErrorDetailsDisplayEventArgs> ErrorDetailsDisplay;

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        ///     Subscribe to this event only if the default implementation calling WPF ChangeLog Dialog to
        ///     display detected updates information doesn't fit your need.
        /// </summary>
        public event EventHandler<PendingUpdateDetailsDisplayEventArgs> UpdateDetailsDisplay;

        /// <summary>
        ///     Subscribe to this event only if the default implementation calling IMessageBoxService.Error
        ///     method to display the errors occurred during the update process started after the application
        ///     closing does not fit your needs.
        /// </summary>
        public event EventHandler<UpdateDetailsDisplayEventArgs> UpdateFailureDetailsDisplay;

        /// <summary>
        ///     Subscribe to this event only if the default implementation calling WPF ChangeLog Dialog to
        ///     display just installed updates change log doesn't fit your need.
        /// </summary>
        public event EventHandler<UpdateDetailsDisplayEventArgs> UpdateSuccessDetailsDisplay;

        public bool CanShowStatusDetails
        {
            get
            {
                return this.ShowStatusDetailsCommand.CanExecute(null);
            }
        }

        public ICommand DownloadAndInstallUpdateCommand { get; private set; }

        public bool ErrorOccurred
        {
            get
            {
                return this.Status == UpdateStatus.InitializationError || this.Status == UpdateStatus.MonitoringError ||
                       this.Status == UpdateStatus.UpdateDownloadingError || this.Status == UpdateStatus.UpdateFailed;
            }
        }

        public ICommand HideCommand { get; private set; }

        public ICommand RestartApplicationCommand { get; private set; }

        public ICommand ShowStatusDetailsCommand { get; private set; }

        public ICommand ShowUpdateDetailsCommand { get; private set; }

        public UpdateStatus Status
        {
            get
            {
                return this.status;
            }
            private set
            {
                if (value == this.status)
                {
                    return;
                }
                this.status = value;
                this.OnPropertyChanged("Status");
                this.OnPropertyChanged("ErrorOccurred");
                this.OnPropertyChanged("UpdatesDetected");
                this.OnPropertyChanged("UpdateIsReadyToInstall");
                CommandManager.InvalidateRequerySuggested();
            }
        }

        public string StatusText
        {
            get
            {
                return this.statusText;
            }
            private set
            {
                if (value == this.statusText)
                {
                    return;
                }
                this.statusText = value;
                this.OnPropertyChanged("StatusText");
            }
        }

        public bool UpdateIsReadyToInstall
        {
            get
            {
                return this.Status == UpdateStatus.UpdateIsReadyToInstall;
            }
        }

        public UpdateManager UpdateManager { get; private set; }

        public bool UpdatesDetected
        {
            get
            {
                return this.Status == UpdateStatus.UpdateDetected;
            }
        }

        public bool UserInteractionIsRequired
        {
            get
            {
                return this.userInteractionIsRequired;
            }
            private set
            {
                this.userInteractionIsRequired = value;
                this.OnPropertyChanged("UserInteractionIsRequired");
            }
        }

        private bool CanDownloadAndInstallUpdate
        {
            get
            {
                return this.UpdateManager.IsUpdateRequired;
            }
        }

        private bool CanShowUpdateDetails
        {
            get
            {
                return this.UpdateManager.IsUpdateRequired;
            }
        }

        private Action ShowStatusDetailsHandler
        {
            get
            {
                return this.showStatusDetailsHandler;
            }
            set
            {
                this.showStatusDetailsHandler = value;
                this.OnPropertyChanged("CanShowStatusDetails");
            }
        }

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private void DownloadAndInstallUpdate()
        {
            this.UserInteractionIsRequired = false;
            string statusTextBase = Resources.UpdateIsDownloading;
            this.Status = UpdateStatus.UpdateIsDownloading;
            this.StatusText = statusTextBase;

            // This timer will update status text dots until the update actions is not finished.
            int dotsCount = 0;
            this.timer = new DispatcherTimer();
            this.timer.Tick += delegate
            {
                this.Status = UpdateStatus.UpdateIsDownloading;
                this.StatusText = statusTextBase + new string('.', dotsCount);
                dotsCount = (dotsCount + 1) % 3;
            };
            this.timer.Interval = TimeSpan.FromMilliseconds(500);
            this.timer.Start();

            // The background update actions.
            TaskScheduler uiContext = TaskScheduler.FromCurrentSynchronizationContext();
            Task task = Task.Factory.StartNew(
                () =>
                {
                    // TODO: remove the unnecessary delays
                    //Thread.Sleep(500);
                    this.UpdateManager.Download(info => statusTextBase = info.Message);
                    //Thread.Sleep(500);
                    this.UpdateManager.Prepare(info => statusTextBase = info.Message);
                });
            task.ContinueWith(
                t =>
                {
                    this.timer.Stop();
                    this.Status = UpdateStatus.UpdateIsReadyToInstall;
                    this.StatusText = Resources.UpdateIsReadyToBeInstalled;
                    this.UserInteractionIsRequired = true;
                    this.ShowStatusDetailsHandler = () => this.UserInteractionIsRequired = true;
                },
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnRanToCompletion,
                uiContext);
            task.ContinueWith(
                t =>
                {
                    this.timer.Stop();
                    Exception ex = t.Exception.InnerException;
                    this.Status = UpdateStatus.UpdateDownloadingError;
                    this.StatusText = Resources.FailedToDownloadTheUpdate;
                    this.UserInteractionIsRequired = true;
                    this.logger.Error(ex.GetFullMessage(), ex);
                },
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnFaulted,
                uiContext);
        }

        private void OnUpdatesDetected(object sender, UpdatesDetectedEventArgs e)
        {
            e.StopLoop = true;
            this.ResetStatus();
            this.Status = UpdateStatus.UpdateDetected;
            this.UserInteractionIsRequired = true;
        }

        private void OnUpdatesMonitoringError(object sender, UpdatesMonitoringErrorEventArgs e)
        {
            this.Status = UpdateStatus.MonitoringError;
            this.StatusText = e.Exception.Message;
            this.ShowStatusDetailsHandler =
                this.ErrorDetailsDisplay == null
                    ? () => this.msgBoxService.Error(this.StatusText + " " + e.Exception.GetFullMessage())
                    : (Action)(() => this.ErrorDetailsDisplay(
                        this,
                        new ErrorDetailsDisplayEventArgs(e.Exception, e.Exception.GetFullMessage(), this.Status)));
            this.logger.Warn(e.Exception.Message, e.Exception);
        }

        private void ResetStatus()
        {
            this.Status = UpdateStatus.Unknown;
            this.StatusText = "";
            this.ShowStatusDetailsHandler = null;
        }

        private void RestartApplication()
        {
            if (this.UpdateManager.ApplyUpdatesAfterExit(true))
            {
                this.ApplicationRestartRequest(this, EventArgs.Empty);
            }
        }

        private void ShowUpdateDetails()
        {
            var eventArgs = new PendingUpdateDetailsDisplayEventArgs(
                this.UpdateManager.Configuration.CurrentVersion, this.UpdateManager.ChangeLog);

            if (this.UpdateDetailsDisplay != null)
            {
                this.UpdateDetailsDisplay(this, eventArgs);
            }
            else
            {
                var dlg = new UpdateDetailsControl
                    {
                        DataContext = new HtmlUpdateDetailsViewModel(this.UpdateManager.ChangeLog, true),
                        Owner = Application.Current.MainWindow
                    };
                bool? r = dlg.ShowDialog();
                eventArgs.DownloadAndInstall = (r == true);
            }

            if (eventArgs.DownloadAndInstall)
            {
                this.DownloadAndInstallUpdate();
            }
        }

        private void ShowUpdateSuccessDetails(ChangeLogRecord[] changeLog)
        {
            var dlg = new UpdateDetailsControl
                {
                    DataContext = new HtmlUpdateDetailsViewModel(changeLog, false)
                };
            dlg.ShowDialog();
        }
    }
}
